/***************************************************************************
 *   mgpi_gpuc.c - Mathematical Calulations using GPU                      *
 *   Copyright (C) 2008 by cmaster.matso                                   *
 *   cmaster.matso@gmail.com                                               *
 *                                                                         *
 *   This program is free software; you can redistribute it and/or modify  *
 *   it under the terms of the GNU General Public License as published by  *
 *   the Free Software Foundation; either version 2 of the License, or     *
 *   (at your option) any later version.                                   *
 *                                                                         *
 *   This program is distributed in the hope that it will be useful,       *
 *   but WITHOUT ANY WARRANTY; without even the implied warranty of        *
 *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the         *
 *   GNU General Public License for more details.                          *
 *                                                                         *
 *   You should have received a copy of the GNU General Public License     *
 *   along with this program; if not, write to the                         *
 *   Free Software Foundation, Inc.,                                       *
 *   59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.             *
 ***************************************************************************/

#include "mgpi_gpuc.h"

static int s_fOnceFlag = 0, s_fVectorComputMode = MGPI_CPU_VECTOR_CALC;
static GLuint s_nFramebuffer = 0;
static char *s_pVertexShaderCode = NULL, *s_pFragmetShaderCode = NULL;
static GPUparam_t *s_pGPURoutineParam = NULL;
static GPUkernel_t s_oComputationsKernel = {MGPI_KERNEL_NOT_INIT, 0, 0, 0};
static GPUroutine_t *s_pRoutinesList = NULL;

/**
 * Updates the GL machine state.
 */
static void s_update(int size) {
    if (size <= 0) {
        MGPI_PostMessagef("%s: given stream size is inappropriate: size = %d", __FUN__, size);
        return;
    }

    glMatrixMode(GL_PROJECTION);
    glLoadIdentity();
    gluOrtho2D(0.0, size, 0.0, size);
    glMatrixMode(GL_MODELVIEW);
    glLoadIdentity();
    glViewport(0, 0, size, size);

    return;
}

/**
 * Initializes the computations kernel.
 */
static int s_initKernel(void) {
    if (s_oComputationsKernel.usage >= 0) {
        MGPI_PostMessagef("%s: cannot initialize kernel: kernel usage flag is not subzero", __FUN__);
        return 0;
    }

    s_oComputationsKernel.usage++;
    s_oComputationsKernel.glsl_program = glCreateProgram();
    s_oComputationsKernel.fragment_shader = glCreateShader(GL_FRAGMENT_SHADER);
    s_oComputationsKernel.vertex_shader = glCreateShader(GL_VERTEX_SHADER);
    return (!0);
}

/**
 * Initializes computations using GPU streams.
 */
static int s_initGPUComput(int *argc, char **argv) {
    glutInit(argc, argv);
    glutCreateWindow(NULL);
    glewInit();

    if (!glewIsSupported("GL_VERSION_2_0")) {
        MGPI_PostMessagef("%s: no OpenGL 2.0 found", __FUN__);
        return 0;
    }

    if (!s_initKernel()) return 0;

    glGenFramebuffersEXT(1, &s_nFramebuffer);
    glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, s_nFramebuffer);
    return (!0);
}

/**
 * Verifies the shaders and GLSL program usege.
 */
static int s_verifyGLSL(GLuint obj, int type) {
    int infologLength = 0;
    int charsWritten = 0;
    char *infoLog;

    switch (type) {
        case MGPI_GLSL_PROGRAM:
            glGetProgramiv(obj, GL_INFO_LOG_LENGTH, &infologLength);
            if (infologLength > 1) {
                infoLog = (char *) calloc(infologLength, sizeof (char));
                glGetProgramInfoLog(obj, infologLength, &charsWritten, infoLog);
                MGPI_PostMessagef("%s: program: \n%s", __FUN__, infoLog);
                free(infoLog);
                return 0;
            }
            break;

        case MGPI_GLSL_SHADER:
            glGetShaderiv(obj, GL_INFO_LOG_LENGTH, &infologLength);
            if (infologLength > 1) {
                infoLog = (char *) calloc(infologLength, sizeof (char));
                glGetShaderInfoLog(obj, infologLength, &charsWritten, infoLog);
                MGPI_PostMessagef("%s: shader: \n%s", __FUN__, infoLog);
                free(infoLog);
                return 0;
            }
        default: break;
    }
    return (!0);
}

/**
 * Sets up the shaders given.
 */
static void s_setupShaders(void) {
    MGPI_BOOL flag = MGPI_FALSE;
    int i = 0;
    GLsizei count, maxCount = 16;
    GLuint *_shaders = (GLuint *) calloc(maxCount, sizeof (GLuint));

    if (glIsProgram(s_oComputationsKernel.glsl_program)) {
        glGetAttachedShaders(s_oComputationsKernel.glsl_program, maxCount, &count, _shaders);

        if (count > 0) {
            while (i < count) {
                if (glIsShader(_shaders[i]))
                    glDetachShader(s_oComputationsKernel.glsl_program, _shaders[i]);
                ++i;
            }
        }
    }
    free(_shaders);

    if (s_pVertexShaderCode) {
        glShaderSource(s_oComputationsKernel.vertex_shader, 1,
                (const char **) & s_pVertexShaderCode, NULL);

        glCompileShader(s_oComputationsKernel.vertex_shader);
        if (!s_verifyGLSL(s_oComputationsKernel.vertex_shader, MGPI_GLSL_SHADER)) {
        	ENGINE_Exit();
        	exit(EXIT_FAILURE);
        }

        glAttachShader(s_oComputationsKernel.glsl_program,
                s_oComputationsKernel.vertex_shader);

        flag = MGPI_TRUE;
    }

    if (s_pFragmetShaderCode) {
        glShaderSource(s_oComputationsKernel.fragment_shader, 1,
                (const char **) & s_pFragmetShaderCode, NULL);

        glCompileShader(s_oComputationsKernel.fragment_shader);
        if (!s_verifyGLSL(s_oComputationsKernel.fragment_shader, MGPI_GLSL_SHADER)) {
        	ENGINE_Exit();
        	exit(EXIT_FAILURE);
        }

        glAttachShader(s_oComputationsKernel.glsl_program,
                s_oComputationsKernel.fragment_shader);

        flag = MGPI_TRUE;
    }

    if (flag) {
        if (glIsProgram(s_oComputationsKernel.glsl_program)) {

            glLinkProgram(s_oComputationsKernel.glsl_program);
            if (!s_verifyGLSL(s_oComputationsKernel.glsl_program, MGPI_GLSL_PROGRAM)) {
            	ENGINE_Exit();
            	exit(EXIT_FAILURE);
            }

            glUseProgram(s_oComputationsKernel.glsl_program);
        }
    }

    return;
}

/**
 * Sets a uniform variable for the GPU routine.
 */
int MGPI_SetGPUVariable(int type, const char *name, ...) {
    GLint location = -1;
    va_list ap;

    location = glGetUniformLocation(s_oComputationsKernel.glsl_program, name);

    va_start(ap, name);

    switch (type) {
        case MGPI_GLSL_FLOAT1:
            glUniform1f(location, va_arg(ap, GLdouble));
            break;

        case MGPI_GLSL_FLOAT2:
            glUniform2f(location, va_arg(ap, GLdouble),
                    va_arg(ap, GLdouble));
            break;

        case MGPI_GLSL_FLOAT3:
            glUniform3f(location, va_arg(ap, GLdouble),
                    va_arg(ap, GLdouble),
                    va_arg(ap, GLdouble));
            break;

        case MGPI_GLSL_FLOAT4:
            glUniform4f(location, va_arg(ap, GLdouble),
                    va_arg(ap, GLdouble),
                    va_arg(ap, GLdouble),
                    va_arg(ap, GLdouble));
            break;

        case MGPI_GLSL_FLOATnv:
            glUniform4fv(location, va_arg(ap, GLsizei), va_arg(ap, GLfloat *));
            break;

        case MGPI_GLSL_INT1:
            glUniform1i(location, va_arg(ap, GLint));
            break;

        case MGPI_GLSL_INT2:
            glUniform2i(location, va_arg(ap, GLint),
                    va_arg(ap, GLint));
            break;

        case MGPI_GLSL_INT3:
            glUniform3i(location, va_arg(ap, GLint),
                    va_arg(ap, GLint),
                    va_arg(ap, GLint));
            break;

        case MGPI_GLSL_INT4:
            glUniform4i(location, va_arg(ap, GLint),
                    va_arg(ap, GLint),
                    va_arg(ap, GLint),
                    va_arg(ap, GLint));
            break;

        case MGPI_GLSL_INTnv:
            glUniform4iv(location, va_arg(ap, GLsizei), va_arg(ap, GLint *));
            break;

        case MGPI_GLSL_MATRIX2:
            glUniformMatrix2fv(location, va_arg(ap, GLsizei), va_arg(ap, GLint), va_arg(ap, GLfloat *));
            break;

        case MGPI_GLSL_MATRIX3:
            glUniformMatrix3fv(location, va_arg(ap, GLsizei), va_arg(ap, GLint), va_arg(ap, GLfloat *));
            break;

        case MGPI_GLSL_MATRIX4:
            glUniformMatrix4fv(location, va_arg(ap, GLsizei), va_arg(ap, GLint), va_arg(ap, GLfloat *));
            break;

        case MGPI_GLSL_MATRIX2x3:
            glUniformMatrix2x3fv(location, va_arg(ap, GLsizei), va_arg(ap, GLint), va_arg(ap, GLfloat *));
            break;

        case MGPI_GLSL_MATRIX3x2:
            glUniformMatrix3x2fv(location, va_arg(ap, GLsizei), va_arg(ap, GLint), va_arg(ap, GLfloat *));
            break;

        case MGPI_GLSL_MATRIX2x4:
            glUniformMatrix2x4fv(location, va_arg(ap, GLsizei), va_arg(ap, GLint), va_arg(ap, GLfloat *));
            break;

        case MGPI_GLSL_MATRIX4x2:
            glUniformMatrix4x2fv(location, va_arg(ap, GLsizei), va_arg(ap, GLint), va_arg(ap, GLfloat *));
            break;

        case MGPI_GLSL_MATRIX3x4:
            glUniformMatrix3x4fv(location, va_arg(ap, GLsizei), va_arg(ap, GLint), va_arg(ap, GLfloat *));
            break;

        case MGPI_GLSL_MATRIX4x3:
            glUniformMatrix4x3fv(location, va_arg(ap, GLsizei), va_arg(ap, GLint), va_arg(ap, GLfloat *));
            break;

        default: break;
    }

    va_end(ap);

    return (!0);
}

/**
 * Starts the advenced vector calculations.
 */
int MGPI_BeginVectorComputations(int mode, int *argc, char *argv[]) {
    if (!s_fOnceFlag) {
        switch (mode) {
            case MGPI_CPU_VECTOR_CALC:
                break;

            case MGPI_GPU_VECTOR_CALC:
                if (!s_initGPUComput(argc, argv)) {
                	ENGINE_Exit();
                	exit(EXIT_FAILURE);
                }
                break;

            default: mode = MGPI_ERR_VECTOR_CALC;
                break;
        }
        s_fOnceFlag = (!0);
    }
    return (s_fVectorComputMode = mode);
}

/**
 * Allocates a given number of GPU stream objects.
 */
GPUstream_t *MGPI_AllocGPUStreams(int streams_number, int data_size, int data_type, int data_format, int internal_format, int target) {
    GPUstream_t *gpus = (GPUstream_t *) calloc(streams_number, sizeof (GPUstream_t));
    int i;

    for (i = 0; i < streams_number; ++i) {
        glGenTextures(1, &gpus[i].stream_id);
        glBindTexture(target, gpus[i].stream_id);

        /* TODO: custom texture parameters setup issue */
        glTexParameteri(target, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
        glTexParameteri(target, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
        glTexParameteri(target, GL_TEXTURE_WRAP_S, GL_CLAMP);
        glTexParameteri(target, GL_TEXTURE_WRAP_T, GL_CLAMP);

        glTexImage2D(target, 0, internal_format, data_size, data_size, 0, data_format, data_type, 0);
        glBindTexture(target, 0);

        gpus[i].err_flag = MGPI_NO_ERROR;
        gpus[i].n = streams_number;
        gpus[i].size = data_size;
        gpus[i].data_type = data_type;
        gpus[i].stream_format = data_format;
        gpus[i].stream_target = target;
    }
    return gpus;
}

/**
 * Allocates a GPU parameter object.
 */
GPUparam_t *MGPI_AllocGPUParam(GPUstream_t *streams, int action_stream_id, int num_streams) {
    GPUparam_t *gparam = (GPUparam_t *) calloc(1, sizeof (GPUparam_t));

    if (!streams) {
        MGPI_PostMessagef("%s: given streams array is a null pointer: streams = %p", __FUN__, streams);
        return NULL;
    }

    gparam->streams = streams;
    gparam->num_streams = num_streams;
    gparam->action_stream_id = action_stream_id;
    return gparam;
}

/**
 * Reads data from the given GPU stream.
 */
void *MGPI_ReadGPUStream(void *output, GPUstream_t *stream) {
    glReadBuffer(GL_COLOR_ATTACHMENT0_EXT);
    glReadPixels(0, 0, stream->size, stream->size, stream->stream_format, stream->data_type, output);
    return output;
}

/**
 * Writes data to the given GPU stream (for Nvidia graphic cards).
 */
void *MGPI_WriteGPUStreamNV(void *input, GPUstream_t *stream) {
    glBindTexture(stream->stream_target, stream->stream_id);
    glTexSubImage2D(stream->stream_target, 0, 0, 0, stream->size, stream->size,
            stream->stream_format, stream->data_type, input);
    glBindTexture(stream->stream_target, 0);
    return input;
}

/**
 * Writes data to the given GPU stream (for ATI graphics cards).
 */
void *MGPI_WriteGPUStreamATI(void *input, GPUstream_t *stream) {/* 2 buffers needed - read and write */
    glFramebufferTexture2DEXT(GL_FRAMEBUFFER_EXT, GL_COLOR_ATTACHMENT0_EXT,
            stream->stream_target,
            stream->stream_id, 0);
    glDrawBuffer(GL_COLOR_ATTACHMENT0_EXT);
    glRasterPos2i(0, 0);
    glDrawPixels(stream->size, stream->size,
            stream->stream_format, stream->data_type, input);
    glFramebufferTexture2DEXT(GL_FRAMEBUFFER_EXT, GL_COLOR_ATTACHMENT0_EXT,
            stream->stream_target, 0, 0);
    return input;
}

/**
 * Frees a given GPU stream object.
 */
int MGPI_FreeGPUStream(GPUstream_t *stream) {
    glDeleteTextures(stream->n, &stream->stream_id);
    return (!0);
}

/**
 * Frees a given GPU parameter object.
 */
int MGPI_FreeGPUParam(GPUparam_t *gparam) {
    int i = 0;

    while (i < gparam->num_streams) {
        MGPI_FreeGPUStream(gparam->streams + i);
        ++i;
    }

    free(gparam->streams);
    free(gparam);
    return (!0);
}

/**
 * Stops advenced vector calculations.
 */
int MGPI_EndVectorComputations(void) {
    if (s_fOnceFlag)
        switch (s_fVectorComputMode) {
            case MGPI_CPU_VECTOR_CALC:
                break;

            case MGPI_GPU_VECTOR_CALC:
                glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, 0);
                glDeleteFramebuffersEXT(1, &s_nFramebuffer);
                break;

            default: return MGPI_ERR_VECTOR_CALC;
        }
    return (s_fOnceFlag = 0);
}

/**
 * Sets up a default GPU calculations input and output with the given GPU parameter object.
 */
int MGPI_SetupGPUParam(GPUparam_t *gparam) {
    int i = 0, n;

    glFramebufferTexture2DEXT(GL_FRAMEBUFFER_EXT, GL_COLOR_ATTACHMENT0_EXT,
            gparam->streams[gparam->num_streams - 1].stream_target,
            gparam->streams[gparam->num_streams - 1].stream_id, 0);
    glDrawBuffer(GL_COLOR_ATTACHMENT0_EXT);

    glGetIntegerv(GL_MAX_TEXTURE_UNITS, &n);
	MGPI_PostMessagef("%s: note: max number of texture units is %d", __FUN__, n);

    glActiveTexture(GL_TEXTURE0);
    glBindTexture(gparam->streams[IN0].stream_target,
            gparam->streams[IN0].stream_id);

    if (gparam->num_streams > 2) {
        while (i < gparam->num_streams - 2 && i < n) {
            glActiveTexture(GL_TEXTURE1 + i);
            glBindTexture(gparam->streams[IN1 + i].stream_target,
                    gparam->streams[IN1 + i].stream_id);
            ++i;
        }
    }

    return (!0);
}

/**
 * Sets vertex and frgament shaders code for the GPU routine.
 */
int MGPI_SetGPURoutineCode(const char *vs_code, const char *fs_code) {
    s_pVertexShaderCode = (char *) vs_code;
    s_pFragmetShaderCode = (char *) fs_code;
    return (!0);
}

/**
 * Frees calculation kernel.
 */
int MGPI_FreeComputKernel(void) {
    if (s_oComputationsKernel.usage == MGPI_KERNEL_NOT_INIT) {
        MGPI_PostMessagef("%s: cannot free uninitialized kernel", __FUN__);
        return 0;
    }

    s_pGPURoutineParam = NULL;
    s_pFragmetShaderCode = NULL;
    s_pVertexShaderCode = NULL;

    if (glIsProgram(s_oComputationsKernel.glsl_program))
        glDeleteProgram(s_oComputationsKernel.glsl_program);

    if (glIsShader(s_oComputationsKernel.fragment_shader))
        glDeleteShader(s_oComputationsKernel.fragment_shader);

    if (glIsShader(s_oComputationsKernel.vertex_shader))
        glDeleteShader(s_oComputationsKernel.vertex_shader);

    s_oComputationsKernel.usage--;

    if (!s_oComputationsKernel.usage) {
        MGPI_PostMessagef("%s: inappriopriate usage of kernel occured: kernel usage flag is not zero", __FUN__);
        return 0;
    }

    return (!0);
}

/**
 * Registers a GPU routine with the given GPU parameter object.
 */
int MGPI_RegGPURoutine(GPUparam_t *param) {
    GPUroutine_t *temp = NULL;
    GPUroutine_t *iterator = s_pRoutinesList;
    int name = 0;

    s_setupShaders();

    if (!s_pRoutinesList) {
        s_pRoutinesList = (GPUroutine_t *) calloc(1, sizeof (GPUroutine_t));

        s_pRoutinesList->next = NULL;
        s_pRoutinesList->prev = NULL;
        s_pRoutinesList->name = 1;

        s_pRoutinesList->param = param;
        name = s_pRoutinesList->name;
    } else {
        temp = (GPUroutine_t *) calloc(1, sizeof (GPUroutine_t));

        while (iterator->next) iterator = iterator->next;

        iterator->next = temp;
        temp->prev = iterator;
        temp->next = NULL;

        temp->name = temp->prev->name + 1;
        temp->param = param;
        name = temp->name;
    }

    return name;
}

/**
 * Calls a GPU routine of the given name.
 */
int MGPI_CallRoutine(int name) {
    GPUroutine_t *iterator = s_pRoutinesList;
    GPUstream_t *handle;
    int size = 0;

    if (name < 0) {
        MGPI_PostMessagef("%s: invalid routine name given", __FUN__);
        return 0;
    }

    if (iterator) {
        while (iterator->next && iterator->name != name)
            iterator = iterator->next;

        if (iterator->name != name) {
            MGPI_PostMessagef("%s: there is no routine of the given name", __FUN__);
            return 0;
        }

        size = iterator->param->streams[iterator->param->action_stream_id].size;
        handle = iterator->param->streams + iterator->param->action_stream_id;

        s_update(size);

        glEnable(handle->stream_target);

        if (iterator->param->streams[iterator->param->action_stream_id].
                stream_target == GL_TEXTURE_2D) {
            glPolygonMode(GL_FRONT, GL_FILL);
            glBegin(GL_QUADS);
            {
                glTexCoord2f(0.0, 0.0);
                glVertex2f(0.0, 0.0);
                glTexCoord2f(1.0, 0.0);
                glVertex2f(size, 0.0);
                glTexCoord2f(1.0, 1.0);
                glVertex2f(size, size);
                glTexCoord2f(0.0, 1.0);
                glVertex2f(0.0, size);
            }
            glEnd();
        } else {
            glPolygonMode(GL_FRONT, GL_FILL);
            glBegin(GL_QUADS);
            {
                glTexCoord2f(0.0, 0.0);
                glVertex2f(0.0, 0.0);
                glTexCoord2f(size, 0.0);
                glVertex2f(size, 0.0);
                glTexCoord2f(size, size);
                glVertex2f(size, size);
                glTexCoord2f(0.0, size);
                glVertex2f(0.0, size);
            }
            glEnd();
        }

        glDisable(handle->stream_target);
        glUseProgram(0);

    } else {
        MGPI_PostMessagef("%s: no routines registered yet", __FUN__);
        return 0;
    }

    return (!0);
}

/**
 * Frees a GPU routine of the given name.
 */
int MGPI_FreeGPURoutine(int name) {
    GPUroutine_t *iterator = s_pRoutinesList;
    GPUroutine_t *temp;

    if (name < 0) {
        MGPI_PostMessagef("%s: invalid routine name given", __FUN__);
        return 0;
    }

    if (iterator) {
        while (iterator->next && iterator->name != name)
            iterator = iterator->next;

        if (iterator->name != name) {
            MGPI_PostMessagef("%s: there is no routine of the given name", __FUN__);
            return 0;
        }

        if (iterator->next && iterator->prev) {
            temp = iterator->next;
            temp->prev = iterator->prev;
            iterator->prev->next = temp;
            iterator->next = NULL;
            iterator->prev = NULL;
            free(iterator);
        } else if (!iterator->next && iterator->prev) {
            temp = iterator->prev;
            temp->next = NULL;
            iterator->prev = NULL;
            free(iterator);
        } else if (!iterator->prev && iterator->next) {
            temp = iterator->next;
            temp->prev = NULL;
            iterator->next = NULL;
            s_pRoutinesList = temp;
            free(iterator);
        } else if (!iterator->next && !iterator->prev) {
            free(iterator);
            iterator = NULL;
        }
    } else {
        MGPI_PostMessagef("%s: no routines registered yet", __FUN__);
        return 0;
    }

    return (!0);
}
